﻿// Copyright (c) Microsoft Corporation
//
// All rights reserved. 
//
// Licensed under the Apache License, Version 2.0 (the ""License""); you may not use this file 
// except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING 
// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY 
// OR NON-INFRINGEMENT. 
//
// See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.

using SQLite;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using TED.WindowsAzure.Unplugged.Mobile.Sync.File;
using Windows.Storage;

namespace TED.WindowsAzure.Unplugged.Mobile.Sync.Storage
{
    /// <summary>
    /// This class implements the storage repository for local file state. SQLite is
    /// used as the database and Windows file storage is used to store the 
    /// database file
    /// </summary>
    public class DefaultSyncFileStateStorage : ISyncFileStateStorage
    {
        private const bool SQLTRACE = false;
        private const int MAXRETRIES = 1000;

        private const string BUSY = "busy";
        private const string DATABASELOCKED = "database is locked";
        private static string _dbPath = string.Empty;
        private readonly TimeSpan BUSYTIMEOUTWAIT = new TimeSpan(0, 0, 0, 0, 1000);

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public DefaultSyncFileStateStorage()
        {
            _dbPath = Path.Combine(ApplicationData.Current.LocalFolder.Path, "blobinfo.sqlite");
            
            // Initialize the database if necessary
            lock (this)
            {
                using (var connection = new SQLiteConnection(_dbPath))
                {
                    connection.Trace = SQLTRACE;

                    // Create the tables if they don't exist
                    try
                    {
                        connection.CreateTable<DefaultSyncFileState>();
                    }
                    catch (SQLiteException)
                    {
                        // Continue if they do exist
                    }

                    connection.Close();
                } 
            }
        }

        public Task<ISyncFileState> CreateFileAsync()
        {
            return Task.Factory.StartNew(() => new DefaultSyncFileState() as ISyncFileState);
        }

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task<IEnumerable<ISyncFileState>> ReadAsync(string folderName)
        {
            return Task.Factory.StartNew(() =>
            {
                var results = new List<ISyncFileState>();

                bool retry = true;
                int retries = 0;

                while (retry)
                {
                    retry = false;

                    // Open SQL Lite Connection
                    lock (this)
                    {
                        using (var connection = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                connection.BusyTimeout = BUSYTIMEOUTWAIT;
                                connection.Trace = SQLTRACE;

                                // Get all local files
                                results = connection.Table<DefaultSyncFileState>()
                                        .Where(b => b.FolderName == folderName)
                                        .OrderBy(b => b.FileName)
                                        .ToList<ISyncFileState>();
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    connection.Close();
                                    throw;
                                }
                            }

                            connection.Close();
                        } 
                    }
                }

                return (IEnumerable<ISyncFileState>) results;
            });
        }

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task<IEnumerable<ISyncFileState>> ReadAsync(string folderName, SyncFileStatus status)
        {
            return Task.Factory.StartNew(() =>
            {
                var results = new List<ISyncFileState>();
                int retries = 0;
                bool retry = true;

                while (retry)
                {
                    retry = false;

                    // Open SQL Lite Connection
                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // Get all local files
                                results = conn.Table<DefaultSyncFileState>()
                                        .Where(b => b.FolderName == folderName && b.StatusAsInt == (int)status)
                                        .OrderBy(b => b.FileName)
                                        .ToList<ISyncFileState>();
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        } 
                    }
                }

                return (IEnumerable<ISyncFileState>) results;
            });
        }

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task<ISyncFileState> LookupAsync(int id)
        {
            return Task.Factory.StartNew(() =>
            {
                ISyncFileState existingFile = null;
                bool retry = true;
                int retries = 0;

                while (retry)
                {
                    retry = false;
                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // search for file in database
                                existingFile = (conn.Table<DefaultSyncFileState>()
                                    .SingleOrDefault(b => b.Id == id));
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        } 
                    }
                }

                return existingFile;
            });
        }

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task<ISyncFileState> LookupAsync(string folderName, string name)
        {
            return Task.Factory.StartNew(() =>
            {
                bool retry = true;
                ISyncFileState existingFile = null;
                int retries = 0;

                while (retry)
                {
                    retry = false;

                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // search for file in database
                                existingFile = (conn.Table<DefaultSyncFileState>()
                                    .SingleOrDefault(b => b.FileName == name && b.FolderName == folderName));
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        } 
                    }
                }

                return existingFile;
            });
        }

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task InsertAsync(ISyncFileState instance)
        {
            Debug.WriteLine("Inserting file {0}", instance.FileName);
            if (instance == null)
                throw new ArgumentNullException("instance");

            return Task.Factory.StartNew(() =>
            {
                instance.Status = SyncFileStatus.Undefined;
                instance.PreviousStatus = SyncFileStatus.Undefined;
                bool retry = true;
                int retries = 0;

                while (retry)
                {
                    retry = false;

                    // Open SQL Lite Connection
                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // Check whether the file is already in the DB
                                DefaultSyncFileState persistedFile = (conn.Table<DefaultSyncFileState>()
                                    .SingleOrDefault(b => b.FileName == instance.FileName));

                                // If file exists
                                if (persistedFile != null)
                                {
                                    // Update it, by copying over the values
                                    throw new ArgumentException("File already exists", "instance");
                                }

                                // the local file does not exist so create it
                                conn.Insert(instance);
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        } 
                    }
                }
            });
        }

        /// <summary>
        ///     If the instance does not exist then a FileNotFoundException will be thrown
        /// </summary>
        /// <param name="instance"></param>
        /// <returns></returns>
        public Task UpdateAsync(ISyncFileState instance)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            return Save(instance, SyncFileStatus.Updated);
        }

        /// <summary>
        ///     If the instance does not exist then a FileNotFoundException will be thrown
        /// </summary>
        /// <param name="instance"></param>
        /// <param name="status"></param>
        /// <returns></returns>
        public Task UpdateAsync(ISyncFileState instance, SyncFileStatus status)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            return Save(instance, status);
        }

        /// <summary>
        ///     Marks the file as deleted allowing a push to recognize when files need to be removed from the server
        /// </summary>
        /// <param name="instance"></param>
        /// <param name="mode"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task DeleteAsync(ISyncFileState instance, DeleteMode mode = DeleteMode.Soft)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            return Task.Factory.StartNew(() =>
            {
                bool retry = true;
                int retries = 0;

                while (retry)
                {
                    retry = false;

                    // Open SQL Lite Connection
                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // Get the local file from the database
                                DefaultSyncFileState persistedFile = conn.Table<DefaultSyncFileState>()
                                    .SingleOrDefault(b => b.Id == instance.Id);

                                // If it doesn't exist throw exception
                                if (persistedFile == null)
                                    throw new FileNotFoundException();

                                persistedFile.Status = persistedFile.Status;
                                persistedFile.Status = SyncFileStatus.Deleted;

                                if (mode == DeleteMode.Soft)
                                {
                                    // Try updating it
                                    if (conn.Update(persistedFile) < 0)
                                        throw new InvalidOperationException("Soft Delete failed");
                                }
                                else
                                {
                                    // Try deleting it
                                    if (conn.Delete(persistedFile) < 0)
                                        throw new InvalidOperationException("Hard Delete failed");
                                }
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        } 
                    }
                }
            });
        }

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task TruncateAsync(string folderName)
        {
            return Task.Factory.StartNew(() =>
            {
                bool retry = true;
                int retries = 0;

                while (retry)
                {
                    retry = false;

                    // Open SQL Lite Connection
                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // Get the local file from the database
                                TableQuery<DefaultSyncFileState> persistedFiles = conn.Table<DefaultSyncFileState>()
                                    .Where(b => b.FolderName == folderName);

                                // not doing a DeleteAll as that removes all records regardless of filter
                                foreach (DefaultSyncFileState p in persistedFiles)
                                    conn.Delete(p);
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        }
                    } 
                }
            });
        }

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task CancelAsync(string folderName)
        {
            return Task.Factory.StartNew(() =>
            {
                bool retry = true;
                int retries = 0;

                while (retry)
                {
                    retry = false;

                    // Open SQL Lite Connection
                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // Get the local file from the database
                                List<ISyncFileState> persistedFiles = conn.Table<DefaultSyncFileState>()
                                    .Where(b => b.StatusAsInt == (int)SyncFileStatus.Undefined).ToList<ISyncFileState>();

                                // roll the status back 
                                foreach (ISyncFileState p in persistedFiles)
                                {
                                    if (p.PreviousStatus == SyncFileStatus.Undefined)
                                        conn.Delete(p);
                                    else
                                    {
                                        p.Status = p.PreviousStatus;
                                        conn.Update(p);
                                    }
                                }
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        } 
                    }
                }
            });
        }

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task UndoAsync(ISyncFileState instance)
        {
            return Task.Factory.StartNew(() =>
            {
                bool retry = true;

                while (retry)
                {
                    retry = false;
                    int retries = 0;

                    // Open SQL Lite Connection
                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // Get the local file from the database
                                DefaultSyncFileState persistedFile =
                                    conn.Table<DefaultSyncFileState>().SingleOrDefault(b => b.Id == instance.Id);

                                if (persistedFile != null)
                                {
                                    persistedFile.Status = persistedFile.PreviousStatus;

                                    if (persistedFile.Status == SyncFileStatus.Undefined)
                                    {
                                        if (conn.Delete(persistedFile) < 0)
                                            throw new InvalidOperationException("Undo failed");
                                    }
                                    else
                                    {
                                        if (conn.Update(persistedFile) < 0)
                                            throw new InvalidOperationException("Undo failed");
                                    }
                                }
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        } 
                    }
                }
            });
        }

        /// <summary>
        ///     Simply marks the file as work in progress, i.e. marks it as undefined
        /// </summary>
        /// <param name="instance"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public Task OpenWriteAsync(ISyncFileState instance)
        {
            return Task.Factory.StartNew(() =>
            {
                bool retry = true;

                while (retry)
                {
                    retry = false;
                    int retries = 0;

                    // Open SQL Lite Connection
                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // Get the local file from the database
                                DefaultSyncFileState persistedFile =
                                    conn.Table<DefaultSyncFileState>().SingleOrDefault(b => b.Id == instance.Id);

                                if (persistedFile != null)
                                {
                                    persistedFile.PreviousStatus = persistedFile.Status;
                                    persistedFile.Status = SyncFileStatus.Undefined;

                                    if (conn.Update(persistedFile) < 0)
                                        throw new InvalidOperationException("Open Write failed");
                                }
                                else
                                {
                                    instance.Status = instance.PreviousStatus = SyncFileStatus.Undefined;

                                    if (conn.Insert(instance) < 0)
                                        throw new InvalidOperationException("Open Write failed");
                                }
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        } 
                    }
                }
            });
        }

        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        private Task Save(ISyncFileState instance, SyncFileStatus status)
        {
            instance.PreviousStatus = (status == SyncFileStatus.Created)
                ? SyncFileStatus.Undefined
                : instance.Status;
            instance.Status = status;

            return Task.Factory.StartNew(() =>
            {
                bool retry = true;
                int retries = 0;

                while (retry)
                {
                    retry = false;

                    // Open SQL Lite Connection
                    lock (this)
                    {
                        using (var conn = new SQLiteConnection(_dbPath))
                        {
                            try
                            {
                                conn.BusyTimeout = BUSYTIMEOUTWAIT;
                                conn.Trace = SQLTRACE;

                                // Check whether the file is already in the DB
                                DefaultSyncFileState persistedFile = (conn.Table<DefaultSyncFileState>()
                                    .SingleOrDefault(b => b.FileName == instance.FileName));

                                // If file exists
                                if (persistedFile == null)
                                {
                                    // Update it, by copying over the values
                                    throw new FileNotFoundException(string.Format("Instance {0} does not exist",
                                        instance.FileName));
                                }

                                persistedFile.ChangedByUser = instance.ChangedByUser;
                                persistedFile.ETag = instance.ETag;
                                persistedFile.PreviousStatus = instance.PreviousStatus;
                                persistedFile.Status = instance.Status;
                                persistedFile.TimeStamp = instance.TimeStamp;

                                // the local file exists so update it
                                conn.Update(persistedFile);
                            }
                            catch (SQLiteException ex)
                            {
                                if (ex.Message.Equals(BUSY, StringComparison.CurrentCultureIgnoreCase) ||
                                    ex.Message.Equals(DATABASELOCKED, StringComparison.CurrentCultureIgnoreCase))
                                {
                                    if (retries < MAXRETRIES)
                                    {
                                        Task.Delay(BUSYTIMEOUTWAIT);
                                        retry = true;
                                    }

                                    retries++;
                                }
                                else
                                {
                                    conn.Close();
                                    throw;
                                }
                            }

                            conn.Close();
                        } 
                    }
                }
            });
        }
    }
}